<?php
// Copyright 2019 Hackware SpA <human@hackware.cl>
// "Hackware Web Services Core" is released under the MIT License terms.

namespace Hawese\Core;

use Illuminate\Support\Collection;

/**
 * Colleciton of TableModels
 *
 * Currently only supports appendForeignObjects() and get()
 */
class TableModelCollection
{
    private $collection;
    private $model;

    public function __construct($collection, $model)
    {
        $this->model = $model;
        $this->collection = $collection->map(function ($object) {
            return new $this->model($object);
        });
    }

    private function mapForeignCollections(array $attributes) : array
    {
        return array_combine(
            $attributes,
            array_map(function ($attribute) {
                $foreign_key = $this->model::guessFK($attribute);
                return $this->getForeignCollection($foreign_key);
            }, $attributes)
        );
    }

    private function getForeignCollection(
        string $foreign_key
    ) : TableModelCollection {
        $ids = $this->collection->pluck($foreign_key)->unique()->values();
        $foreign_model = $this->model::$foreign_keys[$foreign_key];

        return $foreign_model::processCollection(
            $foreign_model::select()
                ->whereIn($foreign_model::$primary_key, $ids)
                ->get()
        );
    }

    /**
     * Match $foreign_collections element PK with $object FK and return it.
     *
     * @param TableModel $object Single object to compare with.
     * @param TableModelCollection[] $foregin_collections map of collections.
     * @param array $match Associative array containing `attribute`, `key` &
     * `model`.
     */
    private function findForeignObject(
        TableModel $object,
        array $foreign_collections,
        array $match
    ) : object {
        return $foreign_collections[$match['attribute']]->get()->first(
            function ($foreign_object) use ($object, $match) {
                $object_fk = $object->{$match['key']};
                $foreign_pk = $foreign_object->{
                    $match['model']::$primary_key
                };
                return $object_fk === $foreign_pk;
            }
        );
    }

    /**
     * Appends $foreign_keys based objects to each object on collection.
     *
     * This method edits the original $collection and is 'fluent'.
     * This avoids the N+1 queries antipattern.
     */
    public function appendForeignObjects(array $attributes) : self
    {
        $foreign_collections = $this->mapForeignCollections($attributes);

        $this->collection = $this->collection->map(
            function ($object) use ($attributes, $foreign_collections) {
                foreach ($attributes as $attribute) {
                    $object->append($attribute);
                    $foreign_key = $this->model::guessFK($attribute);

                    $object->{$attribute} = $this->findForeignObject(
                        $object,
                        $foreign_collections,
                        [
                            'attribute' => $attribute,
                            'key' => $foreign_key,
                            'model' => $this->model::$foreign_keys[$foreign_key]
                        ]
                    );
                }
                return $object;
            }
        );

        return $this;
    }

    public function get() : Collection
    {
        return $this->collection;
    }
}
